﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Text.RegularExpressions;

namespace dnFileEncryptor
{
	public class Core
	{
		/// <summary>
		/// Generates a hash for the given plain text value and returns a
		/// base64-encoded result. Before the hash is computed, a random salt
		/// is generated and appended to the plain text. This salt is stored at
		/// the end of the hash value, so it can be used later for hash
		/// verification.
		/// </summary>
		/// <param name="text">
		/// Plain text value to be hashed. The function does not check whether
		/// this parameter is null.
		/// </param>
		/// <param name="hashAlgorithm">
		/// Hash algorithm.
		/// </param>
		/// <param name="saltBytes">
		/// Salt bytes. This parameter can be null, in which case a random salt value will be generated.
		/// </param>
		/// <returns>Hash value formatted as a base64-encoded string.</returns>
		public static string ComputeHash(string text, HashAlgorithmType hashAlgorithm, byte[] saltBytes)
		{
			// If salt is not specified, generate it on the fly.
			if (saltBytes == null)
			{
				// Define min and max salt sizes.
				int minSaltSize = 4;
				int maxSaltSize = 8;

				// Generate a random number for the size of the salt.
				Random random = new Random();
				int saltSize = random.Next(minSaltSize, maxSaltSize);
				saltBytes = new byte[saltSize];

				RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
				rng.GetNonZeroBytes(saltBytes);
			}

			byte[] plainTextBytes = Encoding.UTF8.GetBytes(text);

			byte[] plainTextWithSaltBytes = new byte[plainTextBytes.Length + saltBytes.Length];

			// Copy plain text bytes into resulting array.
			Array.Copy(plainTextBytes, plainTextWithSaltBytes, plainTextBytes.Length);

			// Append salt bytes to the resulting array.
			Array.Copy(saltBytes, 0, plainTextWithSaltBytes, plainTextBytes.Length, saltBytes.Length);

			// Because we support multiple hashing algorithms, we must define
			// hash object as a common (abstract) base class. We will specify the
			// actual hashing algorithm class later during object creation.
			HashAlgorithm hash;

			// Initialize appropriate hashing algorithm class.
			switch (hashAlgorithm)
			{
				case HashAlgorithmType.SHA1Managed:
					hash = new SHA1Managed();
					break;

				case HashAlgorithmType.SHA256:
					hash = new SHA256Managed();
					break;

				case HashAlgorithmType.SHA384:
					hash = new SHA384Managed();
					break;

				case HashAlgorithmType.SHA512:
					hash = new SHA512Managed();
					break;

				case HashAlgorithmType.MD5:
					hash = new MD5CryptoServiceProvider();
					break;

				case HashAlgorithmType.SHA1:
				default:
					hash = new SHA1CryptoServiceProvider();
					break;
			}

			// Compute hash value of our plain text with appended salt.
			byte[] hashBytes = hash.ComputeHash(plainTextWithSaltBytes);

			// Create array which will hold hash and original salt bytes.
			byte[] hashWithSaltBytes = new byte[hashBytes.Length + saltBytes.Length];

			// Copy hash bytes into resulting array.
			Array.Copy(hashBytes, hashWithSaltBytes, hashBytes.Length);

			// Append salt bytes to the result.
			Array.Copy(saltBytes, 0, hashWithSaltBytes, hashBytes.Length, saltBytes.Length);

			// Convert result into a base64-encoded string.
			return Convert.ToBase64String(hashWithSaltBytes);
		}


		/// <summary>
		/// Compares a hash of the specified plain text value to a given hash
		/// value. Plain text is hashed with the same salt value as the original
		/// hash.
		/// </summary>
		/// <param name="text">
		/// Plain text to be verified against the specified hash. The function
		/// does not check whether this parameter is null.
		/// </param>
		/// <param name="hashAlgorithm">
		/// Hash algorithm.
		/// </param>
		/// <param name="hashValue">
		/// Base64-encoded hash value produced by ComputeHash function. This value
		/// includes the original salt appended to it.
		/// </param>
		/// <returns>
		/// If computed hash mathes the specified hash the function the return
		/// value is true; otherwise, the function returns false.
		/// </returns>
		public static bool VerifyHash(string text, HashAlgorithmType hashAlgorithm, string hashValue)
		{
			// Convert base64-encoded hash value into a byte array.
			byte[] hashWithSaltBytes = Convert.FromBase64String(hashValue);

			int hashSizeInBits;

			// Size of hash is based on the specified algorithm.
			switch (hashAlgorithm)
			{
				case HashAlgorithmType.SHA1Managed:
					hashSizeInBits = 160;
					break;

				case HashAlgorithmType.SHA256:
					hashSizeInBits = 256;
					break;

				case HashAlgorithmType.SHA384:
					hashSizeInBits = 384;
					break;

				case HashAlgorithmType.SHA512:
					hashSizeInBits = 512;
					break;

				case HashAlgorithmType.MD5:
					hashSizeInBits = 128;
					break;

				case HashAlgorithmType.SHA1:
				default:
					hashSizeInBits = 160;
					break;
			}

			// Convert size of hash from bits to bytes.
			int hashSizeInBytes = hashSizeInBits / 8;

			// Make sure that the specified hash value is long enough.
			if (hashWithSaltBytes.Length < hashSizeInBytes)
				return false;

			// Allocate array to hold original salt bytes retrieved from hash.
			byte[] saltBytes = new byte[hashWithSaltBytes.Length - hashSizeInBytes];

			// Copy salt from the end of the hash to the new array.
			for (int i = 0; i < saltBytes.Length; i++)
				saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];

			string expectedHashString = ComputeHash(text, hashAlgorithm, saltBytes);

			return hashValue == expectedHashString;
		}

		/// <summary>
		/// Encrypts input file and writes it to output file. If encryption fails, returns false.
		/// </summary>
		/// <param name="inputFilePath">Input file.</param>
		/// <param name="outputFilePath">File to writes encrypted data to. If file exists it will be overwritten.</param>
		/// <param name="algorithmType">Type of encryption algorithm</param>
		/// <param name="key">A string key</param>
		/// <returns>If encryption fails, returns false.</returns>
		public static void EncryptFile(string inputFilePath, string outputFilePath, EncryptionAlgorithmType algorithmType, string key)
		{
			if (string.IsNullOrEmpty(inputFilePath))
				throw new ArgumentNullException("inputFilePath");

			if (!File.Exists(inputFilePath))
				throw new System.IO.FileNotFoundException("Input file was not found");

			using (FileStream inputStream = File.Open(inputFilePath, FileMode.Open, FileAccess.Read))
			using (FileStream outputStream = File.Open(outputFilePath, FileMode.Create, FileAccess.Write))
			{
				Encrypt(inputStream, outputStream, algorithmType, key);
			}
		}

		/// <summary>
		/// Decrypts input file and writes it to output file. If decryption fails, returns false.
		/// </summary>
		/// <param name="inputFilePath">Input file.</param>
		/// <param name="outputFilePath">File to writes encrypted data to. If file exists it will be overwritten.</param>
		/// <param name="algorithmType">Type of encryption algorithm</param>
		/// <param name="key">A string key</param>
		/// <returns>If decryption fails, returns false.</returns>
		public static void DecryptFile(string inputFilePath, string outputFilePath, EncryptionAlgorithmType algorithmType, string key)
		{
			if (string.IsNullOrEmpty(inputFilePath))
				throw new ArgumentNullException("inputFilePath");

			if (!File.Exists(inputFilePath))
				throw new System.IO.FileNotFoundException("Input file was not found");

			using (FileStream inputStream = File.Open(inputFilePath, FileMode.Open, FileAccess.Read))
			using (FileStream outputStream = File.Open(outputFilePath, FileMode.Create, FileAccess.Write))
			{
				Decrypt(inputStream, outputStream, algorithmType, key);
			}
		}

		/// <summary>
		/// Encrypts string using specified algorithm and key
		/// </summary>
		/// <param name="stringToEncrypt"></param>
		/// <param name="algorithmType"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static string Encrypt(string stringToEncrypt, EncryptionAlgorithmType algorithmType, string key)
		{
			if (stringToEncrypt == null)
				return null;

			return Convert.ToBase64String(Encrypt(Encoding.UTF8.GetBytes(stringToEncrypt), algorithmType, key));
		}

		/// <summary>
		/// Decrypts string using specified algorithm and key
		/// </summary>
		/// <param name="stringToDecrypt"></param>
		/// <param name="algorithmType"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static string Decrypt(string stringToDecrypt, EncryptionAlgorithmType algorithmType, string key)
		{
			if (stringToDecrypt == null)
				return null;

			return Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(stringToDecrypt), algorithmType, key));
		}

		/// <summary>
		/// Encrypts bytes using specified algorithm and key
		/// </summary>
		/// <param name="bytesToEncrypt"></param>
		/// <param name="algorithmType"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static byte[] Encrypt(byte[] bytesToEncrypt, EncryptionAlgorithmType algorithmType, string key)
		{
			if (bytesToEncrypt == null)
				return null;

			using (MemoryStream inputStream = new MemoryStream(bytesToEncrypt))
			using (MemoryStream outputStream = new MemoryStream())
			{
				Encrypt(inputStream, outputStream, algorithmType, key);
				return outputStream.ToArray();
			}
		}

		/// <summary>
		/// Decrypts bytes using specified algorithm and key
		/// </summary>
		/// <param name="bytesToDecrypt"></param>
		/// <param name="algorithmType"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static byte[] Decrypt(byte[] bytesToDecrypt, EncryptionAlgorithmType algorithmType, string key)
		{
			if (bytesToDecrypt == null)
				return null;

			using (MemoryStream inputStream = new MemoryStream(bytesToDecrypt))
			using (MemoryStream outputStream = new MemoryStream())
			{
				Decrypt(inputStream, outputStream, algorithmType, key);
				return outputStream.ToArray();
			}
		}

		/// <summary>
		/// Encrypts input file and writes it to output file. If encryption fails, returns false.
		/// </summary>
		/// <param name="inputStream">Input stream</param>
		/// <param name="outputStream">Output stream. The stream will be closed at the end.</param>
		/// <param name="algorithmType">Type of encryption algorithm</param>
		/// <param name="key">A string key</param>
		/// <returns>If encryption fails, returns false.</returns>
		public static void Encrypt(Stream inputStream, Stream outputStream, EncryptionAlgorithmType algorithmType, string key)
		{
			if (inputStream == null)
				throw new ArgumentNullException("inputStream");

			// Buffer for copying
			byte[] buf = new byte[4096];

			CryptoStream cryptoStream = EncryptionStream(outputStream, algorithmType, key);
			while (true)
			{
				int read = inputStream.Read(buf, 0, buf.Length);
				if (read <= 0)
					break;
				cryptoStream.Write(buf, 0, read);
			}
			cryptoStream.FlushFinalBlock();
		}

		/// <summary>
		/// Returns encryption stream
		/// </summary>
		/// <param name="inputStream"></param>
		/// <param name="algorithmType"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static CryptoStream EncryptionStream(Stream outputStream, EncryptionAlgorithmType algorithmType, string key)
		{
			if (outputStream == null)
				throw new ArgumentNullException("outputStream");

			SymmetricAlgorithm algorithm = null;

			switch (algorithmType)
			{
				case EncryptionAlgorithmType.DES:
					algorithm = new DESCryptoServiceProvider();
					break;
				case EncryptionAlgorithmType.TDES:
					algorithm = new TripleDESCryptoServiceProvider();
					break;
				case EncryptionAlgorithmType.AES:
					algorithm = new AesCryptoServiceProvider();
					break;
			}

			byte[] hashedKey = createHashedKey(key, Constants.HashingAlgorithm, EnumValues.GetEnumValue<int>(algorithmType, "key-size") / 8);
			byte[] iVector = createHashedKey(key, Constants.HashingAlgorithm, EnumValues.GetEnumValue<int>(algorithmType, "block-size") / 8);

			algorithm.Key = hashedKey;
			algorithm.IV = iVector;
			ICryptoTransform transformer = algorithm.CreateEncryptor();

			// Buffer for copying
			byte[] buf = new byte[4096];

			return new CryptoStream(outputStream, transformer, CryptoStreamMode.Write);
		}

		/// <summary>
		/// Decrypts input file and writes it to output file. If decryption fails, returns false.
		/// </summary>
		/// <param name="inputStream">Input stream. The stream will be closed at the end.</param>
		/// <param name="outputStream">Output stream.</param>
		/// <param name="algorithmType">Type of encryption algorithm</param>
		/// <param name="key">A string key</param>
		/// <returns>If decryption fails, returns false.</returns>
		public static void Decrypt(Stream inputStream, Stream outputStream, EncryptionAlgorithmType algorithmType, string key)
		{
			if (inputStream == null)
				throw new ArgumentNullException("inputStream");
			if (outputStream == null)
				throw new ArgumentNullException("outputStream");

			// Buffer for copying
			byte[] buf = new byte[4096];

			CryptoStream cryptoStream = DecryptionStream(inputStream, algorithmType, key);
			while (true)
			{
				int read = cryptoStream.Read(buf, 0, buf.Length);
				if (read <= 0)
					break;
				outputStream.Write(buf, 0, read);
			}
			outputStream.Flush();
		}

		/// <summary>
		/// Returns decryption stream
		/// </summary>
		/// <param name="inputStream"></param>
		/// <param name="algorithmType"></param>
		/// <param name="key"></param>
		/// <returns></returns>
		public static CryptoStream DecryptionStream(Stream inputStream, EncryptionAlgorithmType algorithmType, string key)
		{
			if (inputStream == null)
				throw new ArgumentNullException("inputStream");

			SymmetricAlgorithm algorithm = null;

			switch (algorithmType)
			{
				case EncryptionAlgorithmType.DES:
					algorithm = new DESCryptoServiceProvider();
					break;
				case EncryptionAlgorithmType.TDES:
					algorithm = new TripleDESCryptoServiceProvider();
					break;
				case EncryptionAlgorithmType.AES:
					algorithm = new AesCryptoServiceProvider();
					break;
			}

			byte[] hashedKey = createHashedKey(key, Constants.HashingAlgorithm, EnumValues.GetEnumValue<int>(algorithmType, "key-size") / 8);
			byte[] iVector = createHashedKey(key, Constants.HashingAlgorithm, EnumValues.GetEnumValue<int>(algorithmType, "block-size") / 8);

			algorithm.Key = hashedKey;
			algorithm.IV = iVector;
			ICryptoTransform transformer = algorithm.CreateDecryptor();

			// Buffer for copying
			byte[] buf = new byte[4096];

			return new CryptoStream(inputStream, transformer, CryptoStreamMode.Read);
		}

		/// <summary>
		/// Hashes text and returns a subset of it of specified size
		/// </summary>
		/// <param name="text">Text to hash</param>
		/// <param name="hashAlgorithm">Hashing algorithm</param>
		/// <param name="size">Size of key in bytes</param>
		/// <returns>Key in bytes</returns>
		private static byte[] createHashedKey(string key, HashAlgorithmType hashAlgorithm, int size)
		{
			if (key == null || size <= 0)
				throw new ArgumentNullException("key");

			PasswordDeriveBytes pdb = new PasswordDeriveBytes(key, Encoding.UTF8.GetBytes("salt"));
			return pdb.GetBytes(size);
		}

		public static double GetPasswordComplexity(string password)
		{
			int charSet = 0;
			string passStrength = "";

			charSet = getCharSetUsed(password);

			return Math.Log(Math.Pow(charSet, password.Length)) / Math.Log(2);
		}

		private static int getCharSetUsed(string pass)
		{
			int ret = 0;

			if (containsNumbers(pass))
			{
				ret += 10;
			}

			if (containsLowerCaseChars(pass))
			{
				ret += 26;
			}

			if (containsUpperCaseChars(pass))
			{
				ret += 26;
			}

			if (containsPunctuation(pass))
			{
				ret += 31;
			}

			return ret;
		}

		private static bool containsNumbers(string str)
		{
			Regex pattern = new Regex(@"[\d]");
			return pattern.IsMatch(str);
		}

		private static bool containsLowerCaseChars(string str)
		{
			Regex pattern = new Regex("[a-z]");
			return pattern.IsMatch(str);
		}

		private static bool containsUpperCaseChars(string str)
		{
			Regex pattern = new Regex("[A-Z]");
			return pattern.IsMatch(str);
		}

		private static bool containsPunctuation(string str)
		{
			// regular expression include _ as a valid char for alphanumeric.. 
			// so we need to explicity state that its considered punctuation.
			Regex pattern = new Regex(@"[\W|_]");
			return pattern.IsMatch(str);
		}
	}
}
